﻿#region header

//  ***************************************************************************
// *                                LICENSE
//  ***************************************************************************
//  * "Haplous" .NET Simple 2D Game Framework
//  * 
//  * Copyright © Jonathan Evans 2014-2014
//  * 
//  * This program is free software; you can redistribute it and/or
//  * modify it under the terms of the GNU General Public License
//  * as published by the Free Software Foundation; either version 2
//  * of the License, or (at your option) any later version.
//  * 
//  * This program is distributed in the hope that it will be useful,
//  * but WITHOUT ANY WARRANTY; without even the implied warranty of
//  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  * GNU General Public License for more details.
//  * 
//  * You should have received a copy of the GNU General Public License
//  * along with this program; if not, write to the Free Software
//  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
//  * 
//  ***************************************************************************
//  * 
//  * Contact: opencollar@googlemail.com
//  * 
//  * Website: http://haplous.codeplex.com/
//  *
//  * NuGet:   http://www.nuget.org/packages/haplous/
//  *
//  * File:    Haplous/Haplous/Actor.cs
//  *
//  ***************************************************************************

#endregion

using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Threading;

using Haplous.Core;
using Haplous.Correctness;
using Haplous.Properties;

using JetBrains.Annotations;

namespace Haplous
{
    /// <summary>
    ///     A visual element that can be moved, and which can interact with other elements.
    /// </summary>
    [SuppressMessage("Microsoft.Maintainability", "CA1501:AvoidExcessiveInheritance")]
    [DebuggerDisplay("Actor: Name={Name}; Size={_position.X),{_position.Y); Size={_size.Width),{_size.Height)")]
    public abstract class Actor : SolidGraphicBlock
    {
        /// <summary>
        ///     The dictionary being wrapped.
        /// </summary>
        /// <remarks>
        ///     Concurrent access is controlled by the <see cref="_directorsLock" /> field.
        /// </remarks>
        [NotNull]
        private readonly Dictionary<Mode, Director> _directors = new Dictionary<Mode, Director>();

        /// <summary>
        ///     A lock used to control concurrent access to the <see cref="_directors" /> field.
        /// </summary>
        [NotNull]
        private readonly ReaderWriterLockSlim _directorsLock = new ReaderWriterLockSlim();

        /// <summary>
        ///     Initializes a new instance of the <see cref="ElementBase" /> class.
        /// </summary>
        /// <param name="game">
        ///     The game to which the class belongs.  The <paramref name="game" /> must not be
        ///     <see langword="null" />.
        /// </param>
        /// <param name="name">A name that uniquely identifies this Actor.</param>
        protected Actor([NotNull] Game game, [NotNull] string name) : base(game, name)
        {}

        /// <summary>
        ///     The definition of the director that will be used by this element in the current mode.
        /// </summary>
        [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods")]
        [SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations")]
        [CanBeNull]
        public Director Director
        {
            get
            {
                var mode = Game.ActiveMode;

                if(mode == null)
                    // ReSharper disable once AssignNullToNotNullAttribute
                    throw new ModeException(string.Format(CultureInfo.InvariantCulture, Resources.Error_CannotSetPropertyNoActiveMode, Game.Name));

                return GetDirector(mode);
            }
            set
            {
                var mode = Game.ActiveMode;

                if(mode == null)
                    // ReSharper disable once AssignNullToNotNullAttribute
                    throw new ModeException(string.Format(CultureInfo.InvariantCulture, Resources.Error_CannotSetPropertyNoActiveMode, Game.Name));

                SetDirector(mode, value);
            }
        }

        /// <summary>
        ///     Copies the property values for the element in one mode to another mode.
        /// </summary>
        /// <param name="source">The source mode of the property values.</param>
        /// <param name="destination">The destination mode of the property values.</param>
        /// <param name="excludedProperties">A list of the names of the properties to exclude from the copy.</param>
        public override void CopyModeProperties(Mode source, Mode destination, IEnumerable<string> excludedProperties)
        {
            base.CopyModeProperties(source, destination, excludedProperties);
            EnforceNotDisposed();
            if((excludedProperties == null) || !excludedProperties.Contains("Director"))
                SetDirector(destination, GetDirector(source));
        }

        /// <summary>
        ///     Gets the director.
        /// </summary>
        /// <param name="mode">The mode.</param>
        /// <returns>Director.</returns>
        /// <exception cref="HaplousException"></exception>
        [CanBeNull]
        public Director GetDirector([NotNull] Mode mode)
        {
            mode.ValidateArgument("mode");
            EnforceNotDisposed();

            _directorsLock.EnterUpgradeableReadLock();
            try
            {
                Director item;
                if(_directors.TryGetValue(mode, out item))
                    // ReSharper disable once AssignNullToNotNullAttribute
                    return item;

                _directorsLock.EnterWriteLock();
                try
                {
                    _directors.Add(mode, null);
                    return null;
                }
                finally
                {
                    _directorsLock.ExitWriteLock();
                }
            }
            finally
            {
                _directorsLock.ExitUpgradeableReadLock();
            }
        }

        /// <summary>
        ///     Sets the director.
        /// </summary>
        /// <param name="mode">The mode.</param>
        /// <param name="value">The value.</param>
        /// <exception cref="HaplousException"></exception>
        private void SetDirector([NotNull] Mode mode, [CanBeNull] Director value)
        {
            mode.ValidateArgument("mode");
            EnforceNotDisposed();

            _directorsLock.EnterUpgradeableReadLock();
            try
            {
                Director item;
                if(_directors.TryGetValue(mode, out item))
                {
                    if(item == value)
                        return;

                    _directorsLock.EnterWriteLock();
                    try
                    {
                        _directors[mode] = value;
                    }
                    finally
                    {
                        _directorsLock.ExitWriteLock();
                    }
                }
                else
                {
                    _directorsLock.EnterWriteLock();
                    try
                    {
                        _directors.Add(mode, value);
                    }
                    finally
                    {
                        _directorsLock.ExitWriteLock();
                    }
                }
            }
            finally
            {
                _directorsLock.ExitUpgradeableReadLock();
            }
            OnPropertyChanged("Director");
        }

        /// <summary>
        ///     Resets the position of this block to that it had at the start of the game.
        /// </summary>
        public override void Reset()
        {
            base.Reset();

            var director = Director;
            if(director != null)
                director.ResetDirection();
        }

        /// <summary>
        ///     Snaps the current state of the block's properties for later restoration by a call to <see cref="IBlock.Reset" />.
        /// </summary>
        public override void Snap()
        {
            base.Snap();

            var director = Director;
            if(director != null)
                director.SnapDirection();
        }

        /// <summary>
        ///     Attempts to move to the specified new position and size.
        /// </summary>
        /// <param name="newPosition">The proposed new position of the item that has changed.</param>
        /// <param name="newSize">The proposed new size of the item that has changed.</param>
        /// <returns>The result of the attempt to move.</returns>
        [NotNull]
        internal IDictionary<ISolidGraphicBlock, OutcomeKind> Move([NotNull] PositionInt32 newPosition, [NotNull] SizeInt32 newSize)
        {
            var results = new Dictionary<ISolidGraphicBlock, OutcomeKind>();
            var illegalOverlapDetected = false;

            // Check every other element to see if the move will result in a collision.
            foreach(var layer in Game.Layers)
            {
                Debug.Assert(layer != null, "layer != null");
                foreach(var element in layer.Elements)
                {
                    var solid = element as ISolidGraphicBlock;
                    if(solid != null)
                    {
                        if(solid == this)
                            continue;

                        if(newPosition.IsOverlapping(newSize, solid.Position, solid.Size, Position))
                        {
                            // So, we have overlapping solids - but is it a collision?
                            var outcome = solid.OnColliding(this, Position, Size, newPosition, newSize);
                            results.Add(solid, outcome);
                            illegalOverlapDetected = illegalOverlapDetected || (outcome == OutcomeKind.Bounce);
                        }
                    }
                }
            }

            if(illegalOverlapDetected)
            {
                foreach(var pair in results.Where(p => p.Value == OutcomeKind.Bounce))
                {
                    var item = pair.Key;

                    Debug.Assert(item != null, "item != null");

                    newPosition.CorrectOverlap(newSize, item.Position, item.Size, Position, Size);
                }
            }

            // Now make the move
            Size.Set(newSize);
            Position.Set(newPosition);

            // Now let the actors know the outcome.
            foreach(var collision in results)
            {
                Debug.Assert(collision.Key != null, "collision.Key != null");
                collision.Key.OnCollision(this, collision.Value);
            }

            // And let the caller know what happened.
            return results;
        }
    }
}